home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / totem / plugins / youtube / youtube.py next >
Encoding:
Python Source  |  2009-04-14  |  15.6 KB  |  416 lines

  1. import totem
  2. import gobject, gtk, gconf
  3. gobject.threads_init()
  4. import gdata.service
  5. import urllib
  6. import httplib
  7. import atom
  8. import threading
  9. import time
  10. import re
  11. import os
  12. import random
  13.  
  14. class DownloadThread (threading.Thread):
  15.     def __init__ (self, youtube, url, treeview_name):
  16.         self.youtube = youtube
  17.         self.url = url
  18.         self.treeview_name = treeview_name
  19.         self._done = False
  20.         self._lock = threading.Lock ()
  21.         threading.Thread.__init__ (self)
  22.  
  23.     def run (self):
  24.         try:
  25.             res = self.youtube.service.Get (self.url).entry
  26.         except gdata.service.RequestError:
  27.             """Probably a 503 service unavailable. Unfortunately we can't give an error message, as we're not in the GUI thread"""
  28.             """Just let the lock go and return"""
  29.             res = None
  30.         gobject.idle_add (self.publish_results, res)
  31.  
  32.     def publish_results(self, res):
  33.         self._lock.acquire (True)
  34.         self.youtube.entry[self.treeview_name] = res
  35.         self._done = True
  36.         self._lock.release ()
  37.         return False
  38.  
  39.     @property
  40.     def done (self):
  41.         """ Thread-safe property to know whether the query is done or not """
  42.         self._lock.acquire (True)
  43.         res = self._done
  44.         self._lock.release ()
  45.         return res
  46.  
  47. class CallbackThread (threading.Thread):
  48.     def __init__ (self, callback, *args, **kwargs):
  49.         self.callback = callback
  50.         self.args = args
  51.         self.kwargs = kwargs
  52.         threading.Thread.__init__ (self)
  53.  
  54.     def run (self):
  55.         res = self.callback (*self.args, **self.kwargs)
  56.         while res == True:
  57.             res = self.callback (*self.args, **self.kwargs)
  58.  
  59. class YouTube (totem.Plugin):
  60.     def __init__ (self):
  61.         totem.Plugin.__init__ (self)
  62.         self.debug = False
  63.         self.gstreamer_plugins_present = True
  64.  
  65.         """Search counters (per search type)"""
  66.         self.in_search = {}
  67.         self.search_token = {} # Used as an ID for a search thread
  68.  
  69.         self.max_results = 20
  70.         self.button_down = False
  71.  
  72.         self.search_terms = ""
  73.         self.youtube_id = ""
  74.  
  75.         self.start_index = {}
  76.         self.results = {} # This is just the number of results from the last pagination query
  77.         self.entry = {}
  78.  
  79.         self.current_treeview_name = ""
  80.         self.notebook_pages = []
  81.  
  82.         self.vadjust = {}
  83.         self.liststore = {}
  84.         self.treeview = {}
  85.  
  86.     def activate (self, totem_object):
  87.         """Check for the availability of the flvdemux and souphttpsrc GStreamer plugins"""
  88.         bvw_name = totem_object.get_video_widget_backend_name ()
  89.  
  90.         """If the user's selected 1.5Mbps or greater as their connection speed, grab higher-quality videos
  91.            and drop the requirement for the flvdemux plugin."""
  92.         self.gconf_client = gconf.client_get_default ()
  93.  
  94.         if bvw_name.find ("GStreamer") != -1:
  95.             try:
  96.                 import pygst
  97.                 pygst.require ("0.10")
  98.                 import gst
  99.  
  100.                 registry = gst.registry_get_default ()
  101.                 if registry.find_plugin ("soup") == None:
  102.                     """This means an error will be displayed when they try to play anything"""
  103.                     self.gstreamer_plugins_present = False
  104.             except ImportError:
  105.                 """Do nothing; either it's using xine or python-gstreamer isn't installed"""
  106.  
  107.         """Continue loading the plugin as before"""
  108.         self.builder = self.load_interface ("youtube.ui", True, totem_object.get_main_window (), self)
  109.         self.totem = totem_object
  110.  
  111.         self.search_entry = self.builder.get_object ("yt_search_entry")
  112.         self.search_entry.connect ("activate", self.on_search_entry_activated)
  113.         self.search_button = self.builder.get_object ("yt_search_button")
  114.         self.search_button.connect ("clicked", self.on_search_button_clicked)
  115.         self.progress_bar = self.builder.get_object ("yt_progress_bar")
  116.  
  117.         self.notebook = self.builder.get_object ("yt_notebook")
  118.         self.notebook.connect ("switch-page", self.on_notebook_page_changed)
  119.  
  120.         self.notebook_pages = ["search", "related"]
  121.         for page in self.notebook_pages:
  122.             self.setup_treeview (page)
  123.         self.current_treeview_name = "search"
  124.  
  125.         self.vbox = self.builder.get_object ("yt_vbox")
  126.         self.vbox.show_all ()
  127.         totem_object.add_sidebar_page ("youtube", _("YouTube"), self.vbox)
  128.  
  129.         """Set up the service"""
  130.         self.service = gdata.service.GDataService (account_type = "HOSTED_OR_GOOGLE", server = "gdata.youtube.com")
  131.  
  132.     def deactivate (self, totem):
  133.         totem.remove_sidebar_page ("youtube")
  134.  
  135.     def setup_treeview (self, treeview_name):
  136.         self.start_index[treeview_name] = 1
  137.         self.results[treeview_name] = 0
  138.         self.entry[treeview_name] = None
  139.         self.in_search[treeview_name] = False
  140.  
  141.         """This is done here rather than in the UI file, because UI files parsed in C and GObjects created in Python apparently don't mix."""
  142.         renderer = totem.CellRendererVideo (use_placeholder = True)
  143.         treeview = self.builder.get_object ("yt_treeview_" + treeview_name)
  144.         treeview.set_property ("totem", self.totem)
  145.         treeview.connect ("row-activated", self.on_row_activated)
  146.         treeview.connect_after ("starting-video", self.on_starting_video)
  147.         treeview.insert_column_with_attributes (0, _("Videos"), renderer, thumbnail=0, title=1)
  148.  
  149.         """Add the extra popup menu options. This is done here rather than in the UI file, because it's done for multiple treeviews;
  150.         if it were done in the UI file, the same action group would be used multiple times, which GTK+ doesn't like."""
  151.         ui_manager = treeview.get_ui_manager ()
  152.         action_group = gtk.ActionGroup ("youtube-action-group")
  153.         action = gtk.Action ("open-in-web-browser", _("_Open in Web Browser"), _("Open the video in your web browser"), "gtk-jump-to")
  154.         action_group.add_action_with_accel (action, None)
  155.  
  156.         ui_manager.insert_action_group (action_group, 1)
  157.         ui_manager.add_ui (ui_manager.new_merge_id (),
  158.                    "/ui/totem-video-list-popup/",
  159.                    "open-in-web-browser",
  160.                    "open-in-web-browser",
  161.                    gtk.UI_MANAGER_MENUITEM,
  162.                    False)
  163.         menu_item = ui_manager.get_action ("/ui/totem-video-list-popup/open-in-web-browser")
  164.         menu_item.connect ("activate", self.on_open_in_web_browser_activated)
  165.  
  166.         self.vadjust[treeview_name] = treeview.get_vadjustment ()
  167.         self.vadjust[treeview_name].connect ("value-changed", self.on_value_changed)
  168.         vscroll = self.builder.get_object ("yt_scrolled_window_" + treeview_name).get_vscrollbar ()
  169.         vscroll.connect ("button-press-event", self.on_button_press_event)
  170.         vscroll.connect ("button-release-event", self.on_button_release_event)
  171.  
  172.         self.liststore[treeview_name] = self.builder.get_object ("yt_liststore_" + treeview_name)
  173.         self.treeview[treeview_name] = treeview
  174.         treeview.set_model (self.liststore[treeview_name])
  175.  
  176.     def on_notebook_page_changed (self, notebook, notebook_page, page_num):
  177.         self.current_treeview_name = self.notebook_pages[page_num]
  178.  
  179.     def on_row_activated (self, treeview, path, column):
  180.         if self.debug:
  181.             print "Activating row"
  182.  
  183.         model, rows = treeview.get_selection ().get_selected_rows ()
  184.         iter = model.get_iter (rows[0])
  185.         youtube_id = model.get_value (iter, 3)
  186.  
  187.         """Get related videos"""
  188.         self.youtube_id = youtube_id
  189.         self.start_index["related"] = 1
  190.         self.results["related"] = 0
  191.         if self.in_search == False or self.current_treeview_name == "related":
  192.             self.progress_bar.set_text (_("Fetching related videos..."))
  193.         self.get_results ("/feeds/api/videos/" + urllib.quote (youtube_id) + "/related?max-results=" + str (self.max_results), "related")
  194.  
  195.         if self.debug:
  196.             print "Done activating row"
  197.  
  198.     def get_fmt_string (self):
  199.         if self.gconf_client.get_int ("/apps/totem/connection_speed") >= 10:
  200.             return "&fmt=18"
  201.         else:
  202.             return ""
  203.  
  204.     def resolve_t_param (self, youtube_id):
  205.         """We have to get the t parameter from the actual video page, since Google changed how their URLs work"""
  206.         stream = urllib.urlopen ("http://youtube.com/watch?v=" + urllib.quote (youtube_id))
  207.         regexp1 = re.compile ("swfArgs.*\"t\": \"([^\"]+)\"")
  208.         regexp2 = re.compile ("</head>")
  209.  
  210.         contents = stream.read ()
  211.         if contents != "":
  212.             """Check for the t parameter, which is now in a JavaScript array on the video page"""
  213.             matches = regexp1.search (contents)
  214.             if (matches != None):
  215.                 stream.close ()
  216.                 return matches.group (1)
  217.  
  218.             """Check to see if we've come to the end of the <head> tag; in which case, we should give up"""
  219.             if (regexp2.search (contents) != None):
  220.                 stream.close ()
  221.                 return ""
  222.  
  223.         stream.close ()
  224.         return ""
  225.  
  226.     def on_starting_video (self, treeview, path, user_data):
  227.         """Display an error if the required GStreamer plugins aren't installed"""
  228.         if self.gstreamer_plugins_present == False:
  229.             self.totem.interface_error_with_link (_("Totem cannot play this type of media (%s) because you do not have the appropriate plugins to handle it.") % _("YouTube"),
  230.                                   _("Please install the necessary plugins and restart Totem to be able to play this media."),
  231.                                   "http://www.gnome.org/projects/totem/#codecs",
  232.                                   _("More information about media plugins"),
  233.                                   self.totem.get_main_window ())
  234.             return False
  235.  
  236.         return True
  237.  
  238.     def on_open_in_web_browser_activated (self, action):
  239.         model, rows = self.treeview[self.current_treeview_name].get_selection ().get_selected_rows ()
  240.         iter = model.get_iter (rows[0])
  241.         youtube_id = model.get_value (iter, 3)
  242.  
  243.         """Open the video in the browser"""
  244.         os.spawnlp (os.P_NOWAIT, "xdg-open", "xdg-open", "http://www.youtube.com/watch?v=" + urllib.quote (youtube_id) + self.get_fmt_string ())
  245.  
  246.     def on_button_press_event (self, widget, event):
  247.         self.button_down = True
  248.  
  249.     def on_button_release_event (self, widget, event):
  250.         self.button_down = False
  251.         self.on_value_changed (self.vadjust[self.current_treeview_name])
  252.  
  253.     def on_value_changed (self, adjustment):
  254.         """Load more results when we get near the bottom of the treeview"""
  255.         if not self.button_down and (adjustment.get_value () + adjustment.page_size) / adjustment.upper > 0.8 and self.results[self.current_treeview_name] >= self.max_results:
  256.             if self.current_treeview_name == "search":
  257.                 if self.debug:
  258.                     print "Getting more results for search \"" + self.search_terms + "\" from offset " + str (self.start_index["search"])
  259.                 self.get_results ("/feeds/api/videos?vq=" + urllib.quote_plus (self.search_terms) + "&max-results=" + str (self.max_results) + "&orderby=relevance&start-index=" + str (self.start_index["search"]), "search", False)
  260.             elif self.current_treeview_name == "related":
  261.                 if self.debug:
  262.                     print "Getting more related videos for video \"" + self.youtube_id + "\" from offset " + str (self.start_index["related"])
  263.                 self.get_results ("/feeds/api/videos/" + urllib.quote_plus (self.youtube_id) + "/related?max-results=" + str (self.max_results) + "&start-index=" + str (self.start_index["related"]), "related", False)
  264.  
  265.     def convert_url_to_id (self, url):
  266.         """Find the last clause in the URL; after the last /"""
  267.         return url.split ("/").pop ()
  268.  
  269.     def populate_list_from_results (self, search_token, treeview_name, thread):
  270.         """Check to see if this search has been cancelled"""
  271.         if search_token != self.search_token[treeview_name]:
  272.             return False
  273.  
  274.         """Check and acquire the lock"""
  275.         if not thread.done:
  276.             if self.current_treeview_name == treeview_name:
  277.                 self.progress_bar.pulse ()
  278.             return True
  279.  
  280.         CallbackThread (self.process_next_thumbnail, search_token, treeview_name).start ()
  281.         return False
  282.  
  283.     def process_next_thumbnail (self, search_token, treeview_name):
  284.         """Note that all the calls to gobject.idle_add are so that the respective
  285.            UI function calls are made in the main thread, since process_next_thumbnail
  286.            is run in the CallbackThread thread."""
  287.  
  288.         """Check to see if this search has been cancelled"""
  289.         if search_token != self.search_token[treeview_name]:
  290.             return False
  291.  
  292.         """Return if there are no results (or we've finished)"""
  293.         if self.entry[treeview_name] == None or len (self.entry[treeview_name]) == 0:
  294.             gobject.idle_add (self._clear_ui, treeview_name)
  295.  
  296.             self.entry[treeview_name] = None
  297.             self.in_search[treeview_name] = False
  298.  
  299.             return False
  300.  
  301.         """Only do one result at a time, as the thumbnail has to be downloaded; give them a temporary MRL until the real one is resolved before playing"""
  302.         entry = self.entry[treeview_name].pop (0)
  303.         self.results[treeview_name] += 1
  304.         self.start_index[treeview_name] += 1
  305.         youtube_id = self.convert_url_to_id (entry.id.text)
  306.  
  307.         """Find the content tag"""
  308.         for _element in entry.extension_elements:
  309.             if _element.tag =="group":
  310.                 break
  311.  
  312.         content_elements = _element.FindChildren ("content")
  313.         if len (content_elements) == 0:
  314.             return True
  315.         mrl = content_elements[0].attributes['url']
  316.  
  317.         """Download the thumbnail and store it in a temporary location so we can get a pixbuf from it"""
  318.         thumbnail_url = _element.FindChildren ("thumbnail")[0].attributes['url']
  319.         try:
  320.             filename, headers = urllib.urlretrieve (thumbnail_url)
  321.         except IOError:
  322.             return True
  323.  
  324.         try:
  325.             pixbuf = gtk.gdk.pixbuf_new_from_file (filename)
  326.         except gobject.GError:
  327.             print "Could not open thumbnail " + filename + " for video. It has been left in place for investigation."
  328.             return True
  329.  
  330.         """Don't leak the temporary file"""
  331.         os.unlink (filename)
  332.  
  333.         """Get the video stream MRL"""
  334.         t_param = self.resolve_t_param (youtube_id)
  335.  
  336.         if t_param != "":
  337.             mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + urllib.quote (t_param) + self.get_fmt_string ()
  338.  
  339.         gobject.idle_add (self._append_to_liststore, treeview_name, pixbuf, entry.title.text, mrl, youtube_id, search_token)
  340.  
  341.         return True
  342.  
  343.     def _clear_ui (self, treeview_name):
  344.         """Revert the cursor"""
  345.         window = self.vbox.window
  346.         window.set_cursor (None)
  347.  
  348.         if self.in_search == True or self.current_treeview_name == treeview_name:
  349.             """Only blank the progress bar if we're the only search taking place"""
  350.             self.progress_bar.set_fraction (0.0)
  351.             self.progress_bar.set_text ("")
  352.  
  353.         return False
  354.  
  355.     def _append_to_liststore (self, treeview_name, pixbuf, title, mrl, id, search_token):
  356.         """Check to see if this search has been cancelled"""
  357.         if search_token != self.search_token[treeview_name]:
  358.             return False
  359.  
  360.         if self.in_search == True or self.current_treeview_name == treeview_name:
  361.             self.progress_bar.set_fraction (float (self.results[treeview_name]) / float (self.max_results))
  362.         self.liststore[treeview_name].append ([pixbuf, title, mrl, id])
  363.         return False
  364.  
  365.     def on_search_button_clicked (self, button):
  366.         search_terms = self.search_entry.get_text ()
  367.  
  368.         if self.debug:
  369.             print "Searching for \"" + search_terms + "\""
  370.  
  371.         """Focus the "Search" tab"""
  372.         self.notebook.set_current_page (self.notebook_pages.index ("search"))
  373.  
  374.         self.search_terms = search_terms
  375.         self.start_index["search"] = 1
  376.         self.results["search"] = 0
  377.         self.progress_bar.set_text (_("Fetching search results..."))
  378.         self.get_results ("/feeds/api/videos?vq=" + urllib.quote_plus (search_terms) + "&orderby=relevance&max-results=" + str (self.max_results), "search")
  379.  
  380.     def on_search_entry_activated (self, entry):
  381.         self.search_button.clicked ()
  382.  
  383.     def get_results (self, url, treeview_name, clear = True):
  384.         if self.in_search[treeview_name] == True and clear == False:
  385.             """If we're trying to fetch more results while another search is happening, just cancel"""
  386.             if self.debug:
  387.                 print "Cancelling getting more results due to existing incomplete search."
  388.             self.in_search[treeview_name] = False
  389.             return
  390.         elif clear == False:
  391.             self.results[self.current_treeview_name] = 0
  392.             self.progress_bar.set_text (_("Fetching more videos..."))
  393.  
  394.         """If we're trying to do another full search while one's already happening, just continue as
  395.            normal. The populate_list_from_results function will notice, and cancel the old search."""
  396.  
  397.         if clear:
  398.             self.liststore[treeview_name].clear ()
  399.  
  400.         if self.debug:
  401.             print "Getting results from URL \"" + url + "\""
  402.  
  403.         self.in_search[treeview_name] = True
  404.         self.search_token[treeview_name] = random.random ()
  405.  
  406.         """Give us a nice waiting cursor"""
  407.         window = self.vbox.window
  408.         window.set_cursor (gtk.gdk.Cursor (gtk.gdk.WATCH))
  409.         if self.current_treeview_name == treeview_name:
  410.             self.progress_bar.pulse ()
  411.  
  412.         thread = DownloadThread (self, url, treeview_name)
  413.         gobject.timeout_add (350, self.populate_list_from_results, self.search_token[treeview_name], treeview_name, thread)
  414.         thread.start()
  415.  
  416.